// 前置理解
// arguments 是一个对应于传递给函数的参数的类数组对象。只能用于函数内。ES6推荐使用 ...args
// slice() 方法返回一个新的数组对象，这一对象是一个由 begin 和 end 决定的原数组的浅拷贝（包括 begin，不包括end）。原始数组不会被改变
// instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上
// bind() 方法创建一个新的函数，在 bind() 被调用时，这个新函数的 this 被指定为 bind() 的第一个参数，而其余参数将作为新函数的参数，供调用时使用。

// 实现思路
// 1. 返回一个函数，使用 apply 改变 this 指向
// 2. 维护原型关系，bind 的原型指向 Function
// 3. 如果使用new运算符构造绑定函数，则忽略传入的this。

// 初级版本
Function.prototype.myFirBind = function (oThis) {
  var _this = this;
  var aArgs = Array.prototype.slice.call(arguments, 1);
  return function () {
    return _this.apply(
      oThis,
      aArgs.concat(Array.prototype.slice.call(arguments)),
    );
  };
};
// 初级实现会存在问题，即如果 bind 返回的函数被 new 实例化执行，绑定的this应该被忽略

// MDN 官方 polyfill

Function.prototype.myBind = function (oThis) {
  // 确定调用 bind 方法的是一个函数
  if (typeof this !== 'function') {
    // closest thing possible to the ECMAScript 5
    // internal IsCallable function
    throw new TypeError(
      'Function.prototype.bind - what is trying to be bound is not callable',
    );
  }

  var aArgs = Array.prototype.slice.call(arguments, 1), //这里的arguments是跟oThis一起传进来的实参
    fToBind = this,
    fNOP = function () {},
    fBound = function () {
      return fToBind.apply(
        // 通过 new 调用的函数肯定指向 fNOP
        // 所以检测构造函数的 prototype 属性是否出现在起到过渡作用的fNOP
        this instanceof fNOP ? this : oThis,
        // 获取调用时(fBound)的传参.bind 返回的函数入参往往是这么传递的
        aArgs.concat(Array.prototype.slice.call(arguments)),
      );
    };

  // 维护原型关系，确保修改 bind 返回的函数的原型时，不影响 Function 的原型
  if (this.prototype) {
    // Function.prototype doesn't have a prototype property
    fNOP.prototype = this.prototype;
  }
  fBound.prototype = new fNOP();

  return fBound;
};

// 应用示例
// 1. 创建绑定函数，改变 this 指向

this.x = 9; // 在浏览器中，this 指向全局的 "window" 对象
var module = {
  x: 81,
  getX: function () {
    return this.x;
  },
};

module.getX(); // 81

var retrieveX = module.getX;
retrieveX();
// 返回 9 - 因为函数是在全局作用域中调用的

// 创建一个新函数，把 'this' 绑定到 module 对象
// 新手可能会将全局变量 x 与 module 的属性 x 混淆
var boundGetX = retrieveX.bind(module);
boundGetX(); // 81

// 2. 使一个函数拥有预设的初始参数;
function list() {
  return Array.prototype.slice.call(arguments);
}

function addArguments(arg1, arg2) {
  return arg1 + arg2;
}

var list1 = list(1, 2, 3); // [1, 2, 3]

var result1 = addArguments(1, 2); // 3

// 创建一个函数，它拥有预设参数列表。
var leadingThirtysevenList = list.bind(null, 37);

// 创建一个函数，它拥有预设的第一个参数
var addThirtySeven = addArguments.bind(null, 37);

var list2 = leadingThirtysevenList();
// [37]

var list3 = leadingThirtysevenList(1, 2, 3);
// [37, 1, 2, 3]

var result2 = addThirtySeven(5);
// 37 + 5 = 42

var result3 = addThirtySeven(5, 10);
// 37 + 5 = 42 ，第二个参数被忽略
// 3. 配合 setTimeOut （箭头函数用的场景更多）
// 在默认情况下，使用 window.setTimeout() 时，
// this 关键字会指向 window（或 global）对象。
// 当类的方法中需要 this 指向类的实例时，
// 你可能需要显式地把 this 绑定到回调函数，就不会丢失该实例的引用。

function LateBloomer() {
  this.petalCount = Math.ceil(Math.random() * 12) + 1;
}

// 在 1 秒钟后声明 bloom
LateBloomer.prototype.bloom = function () {
  window.setTimeout(this.declare.bind(this), 1000);
};

LateBloomer.prototype.declare = function () {
  console.log('I am a beautiful flower with ' + this.petalCount + ' petals!');
};

var flower = new LateBloomer();
flower.bloom(); // 一秒钟后，调用 'declare' 方法
// 4. 快捷调用

var slice = Array.prototype.slice;

// ...

slice.apply(arguments);
// 与前一段代码的 "slice" 效果相同
var unboundSlice = Array.prototype.slice;
var slice = Function.prototype.apply.bind(unboundSlice);

// ...

slice(arguments);
